{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {
    "ein.tags": "worksheet-0",
    "slideshow": {
     "slide_type": "-"
    }
   },
   "source": [
    "# 模板\n",
    "把业务逻辑和表现逻辑混在一起会导致代码难以理解和维护。为了提升程序的可维护性，通常把表现逻辑移到 **模板** 中。\n",
    "\n",
    "**模板** 是一个包含响应文本的文件，其中包含用占位变量表示的动态部分， 其具体值只在请求的上下文中才能知道。使用真实值替换变量，再返回最终得到的响应字符串， 这一过程称为 **渲染** 。为了渲染模板，Flask 使用了一个名为 **[Jinja2](http://jinja.pocoo.org/)** 的强大模板引擎。\n",
    "\n",
    "\n",
    "## Jinja2模板引擎\n",
    "\n",
    "\n",
    "### 基本使用\n",
    "\n",
    "Jinja2通过`Template`类创建并渲染模板："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "autoscroll": false,
    "collapsed": false,
    "ein.tags": "worksheet-0",
    "slideshow": {
     "slide_type": "-"
    }
   },
   "outputs": [],
   "source": [
    "from jinja2 import Template\n",
    "template = Template('Hello {{ name }}!')\n",
    "print(template.render(name='Xiao Ming'))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "ein.tags": "worksheet-0",
    "slideshow": {
     "slide_type": "-"
    }
   },
   "source": [
    "其背后通过`Environment`实例来存储配置和全局对象，从文件系统或其他位置加载模板：\n",
    "\n",
    "```django\n",
    "{# templates/user.html #}\n",
    "<h1>Hello, {{ name }}!</h1>\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "autoscroll": false,
    "collapsed": false,
    "ein.tags": "worksheet-0",
    "slideshow": {
     "slide_type": "-"
    }
   },
   "outputs": [],
   "source": [
    "# IPython magic command\n",
    "%cd -q ~/Documents/WebApp/flaskr/\n",
    "!git checkout -q 2a\n",
    "\n",
    "from jinja2 import Environment, PackageLoader\n",
    "\n",
    "\n",
    "env = Environment(loader=PackageLoader('hello', 'templates'))\n",
    "template = env.get_template('user.html')\n",
    "template.render(name='Xiao Ming')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "ein.tags": "worksheet-0",
    "slideshow": {
     "slide_type": "-"
    }
   },
   "source": [
    "通过`Environment`创建了一个模板环境，模板加载器（loader）会在`templates`文件夹中寻找模板。\n",
    "### Flask中使用模板\n",
    "\n",
    "默认情况下，Flask 在程序文件夹中的`templates`子文件夹中寻找模板。\n",
    "\n",
    "形式最简单的 Jinja2 模板就是一个包含响应文本的文件。\n",
    "\n",
    "首先创建`templates`子文件夹，然后在其中创建`index.html`和`user.html`文件。\n",
    "\n",
    "```django\n",
    "{# templates/index.html #}\n",
    "<h1>Hello World!</h1>\n",
    "```\n",
    "\n",
    "视图函数`user()`返回的响应中包含一个使用变量表示的动态部分。 下例使用模板实现这个响应：\n",
    "\n",
    "```django\n",
    "{# templates/user.html #}\n",
    "<h1>Hello, {{ name }}!</h1>\n",
    "```\n",
    "\n",
    "\n",
    "### 渲染模板\n",
    "\n",
    "接下来为`hello.py`增加模板渲染。\n",
    "\n",
    "修改程序中的视图函数，以便渲染上面创建的2个模板。\n",
    "\n",
    "```python\n",
    "# hello.py\n",
    "from flask import Flask, render_template\n",
    "\n",
    "# ...\n",
    "\n",
    "@app.route('/')\n",
    "def index():\n",
    "    return render_template('index.html')\n",
    "\n",
    "\n",
    "@app.route('/user/<name>')\n",
    "def user(name):\n",
    "    return render_template('user.html', name=name)\n",
    "```\n",
    "\n",
    "Flask 提供的`render_template`函数把 Jinja2 模板引擎集成到了程序中。`render_template`函数的第一个参数是模板的文件名。 随后的参数都是键值对，表示模板中变量对应的真实值。\n",
    "\n",
    "**🔖 执行`git checkout 2a`签出程序的这个版本。**\n",
    "\n",
    "\n",
    "### 变量和过滤器\n",
    "\n",
    "在模板中使用的`{{ name }}`结构表示一个变量，它是一种特殊的占位符， 告诉模板引擎这个位置的值从渲染模板时使用的数据中获取。\n",
    "\n",
    "Jinja2 能识别所有类型的变量，甚至是一些复杂的类型，例如列表、字典和对象。\n",
    "\n",
    "使用变量的一些示例：\n",
    "\n",
    "```django\n",
    "<p>A value from a dictionary: {{ mydict['key'] }}.</p>\n",
    "<p>A value from a list: {{ mylist[3] }}.</p>\n",
    "<p>A value from a list, with a variable index: {{ mylist[myintvar] }}.</p>\n",
    "<p>A value from an object's method: {{ myobj.somemethod() }}.</p>\n",
    "```\n",
    "\n",
    "可以使用 **过滤器** 修改变量，过滤器名添加在变量名之后，中间使用竖线（`|`）分隔。\n",
    "\n",
    "```django\n",
    "{# 以首字母大写形式显示变量 name 的值 #}\n",
    "Hello, {{ name|capitalize }}\n",
    "```\n",
    "\n",
    "下标列举了 Jinja2 提供的部分常用过滤器<sup><a id=\"fnr.1\" class=\"footref\" href=\"#fn.1\">1</a></sup>：\n",
    "\n",
    "| 过滤器名     | 说明                  |\n",
    "|--------------|-----------------------|\n",
    "| `safe`       | 渲染值时不转义        |\n",
    "| `capitalize` | 把值的首字母转换成大写，其他字母转换成小写 |\n",
    "| `lower`      | 把值转换成小写形式    |\n",
    "| `upper`      | 把值转换成大写形式    |\n",
    "| `title`      | 把值中每个单词的首字母都转换成大写 |\n",
    "| `trim`       | 把值的首尾空格去掉    |\n",
    "| `striptags`  | 渲染之前把值中所有的HTML标签都删掉 |\n",
    "\n",
    "**默认情况下，出于安全考虑，Jinja2 会转义所有变量。** 例如，如果一个变量的值为`'<h1>Hello</h1>'`，Jinja2 会将其渲染成`'&lt;h1&gt;Hello&lt;/h1&gt;'`，浏览器能显示这个`h1`元素，但不会进行解释。 **很多情况下需要显示变量中存储的 HTML 代码，这时就可使用`safe`过滤器。**\n",
    "\n",
    "**注意：** 千万别在不可信的值上使用`safe`过滤器，例如用户在表单中输入的文本。可能会引发 **跨站脚本攻击（XSS）** 。\n",
    "\n",
    "\n",
    "### 控制结构\n",
    "\n",
    "Jinja2 提供了多种控制结构，可用来改变模板的渲染流程。\n",
    "\n",
    "\n",
    "#### 条件控制\n",
    "\n",
    "```django\n",
    "{% if user %}\n",
    "    Hello, {{ user }}!\n",
    "{% else %}\n",
    "    Hello, Stranger!\n",
    "{% endif %}\n",
    "```\n",
    "\n",
    "\n",
    "#### `for` 循环\n",
    "\n",
    "```django\n",
    "<ul>\n",
    "    {% for comment in comments %}\n",
    "        <li>{{ comment }}</li>\n",
    "    {% endfor %}\n",
    "</ul>\n",
    "```\n",
    "\n",
    "\n",
    "### 宏\n",
    "\n",
    "宏类似于 Python 代码中的函数。\n",
    "\n",
    "```django\n",
    "{% macro render_comment(comment) %}\n",
    "    <li>{{ comment }}</li>\n",
    "{% endmacro %}\n",
    "\n",
    "<ul>\n",
    "    {% for comment in comments %}\n",
    "        {{ render_comment(comment) }}\n",
    "    {% endfor %}\n",
    "</ul>\n",
    "```\n",
    "\n",
    "为了重复使用宏，可以将其保存在单独的文件中，然后在需要使用的模板中导入:\n",
    "\n",
    "```django\n",
    "{% import 'macros.html' as macros %}\n",
    "\n",
    "<ul>\n",
    "    {% for comment in comments %}\n",
    "        {{ macros.render_comment(comment) }}\n",
    "    {% endfor %}\n",
    "</ul>\n",
    "```\n",
    "\n",
    "导入使用与Python中类似的`import`语句，可以直接把整个模板导入到一个变量（`import xxx as yyy`），像上面那样，或者从其中导入特定的宏（`from xxx import yyy`）。\n",
    "\n",
    "```django\n",
    "{% from 'macros.html' import render_comment as r_comment %}\n",
    "```\n",
    "\n",
    "\n",
    "### include 包含\n",
    "\n",
    "为了避免重复，需要在多处重复使用的模板代码片段可以写入单独的文件，再包含在所有模板中。\n",
    "\n",
    "```django\n",
    "{% include 'common.html' %}\n",
    "```\n",
    "\n",
    "渲染时会在`include`语句的对应位置添加被包含的模板内容：\n",
    "\n",
    "```django\n",
    "{% include \"header.html\" %}\n",
    "    Body\n",
    "{% include \"footer.html\" %}\n",
    "```\n",
    "\n",
    "`include`可以使用`ignore missing`标记，如果模板不存在，会直接忽略：\n",
    "\n",
    "```django\n",
    "{% include \"sidebar.html\" ignore missing %}\n",
    "```\n",
    "\n",
    "\n",
    "### 模板继承\n",
    "\n",
    "类似于 Python 代码中的类继承。合理使用模板继承，让模板能重用，能提高工作效率和代码质量。\n",
    "\n",
    "首先，创建一个名为`base.html`的基模板：\n",
    "\n",
    "```django\n",
    "<html>\n",
    "    <head>\n",
    "        {% block head %}\n",
    "            <title>{% block title %}{% endblock %} - My Application</title>\n",
    "        {% endblock %}\n",
    "    </head>\n",
    "    <body>\n",
    "        {% block body %}\n",
    "        {% endblock %}\n",
    "    </body>\n",
    "</html>\n",
    "```\n",
    "\n",
    "`block`标签定义的元素可在衍生模板（子模板）中重载，如果子模板没有重载，就用基模板的定义 显示默认内容。 上面例子中，定义了名为`head`、`title`和`body`的块。`title`包含在`head`中。\n",
    "\n",
    "下面是基模板的衍生模板：\n",
    "\n",
    "```django\n",
    "{% extends \"base.html\" %}\n",
    "\n",
    "{% block title %}Index{% endblock %}\n",
    "{% block head %}\n",
    "    {{ super() }}\n",
    "    <style>\n",
    "    </style>\n",
    "{% endblock %}\n",
    "{% block body %}\n",
    "    <h1>Hello, World!</h1>\n",
    "{% endblock %}\n",
    "```\n",
    "\n",
    "`extends`指令声明这个模板衍生自`base.html`。 在`extends`指令之后，基模板中的 3 个块被重新定义，模板引擎会将其插入适当的位置。 **注意：** 新定义的`head`块，在基模板中其内容不是空的，所以使用`super()`获取原来的内容。\n",
    "\n",
    "\n",
    "### 赋值\n",
    "\n",
    "在代码块中使用`set`标签为变量赋值，并且可以为多个变量赋值：\n",
    "\n",
    "```ipython\n",
    "from jinja2 import Template\n",
    "\n",
    "print(Template(\"\"\"\n",
    "{% set a = 1 %}\n",
    "{% set b, c = range(2) %}\n",
    "<p>{{ a }} {{ b }} {{ c }}</p>\n",
    "\"\"\").render())\n",
    "```\n",
    "\n",
    "\n",
    "\n",
    "\n",
    "    <p>1 0 1</p>\n",
    "\n",
    "\n",
    "## 使用 Flask-Bootstrap\n",
    "\n",
    "Bootstrap是非常流行的前端开发框架。\n",
    "\n",
    "要在程序中集成Bootstrap，需要对模板进行修改，加入 Bootstrap 层叠样式表（CSS） 和 JavaScript 文件的引用。但是，更简单的办法是直接使用Flask扩展 [Flask-Bootstrap](http://pythonhosted.org/Flask-Bootstrap/) 。\n",
    "\n",
    "\n",
    "### 安装 Flask-Bootstrap\n",
    "\n",
    "```sh\n",
    "(flaskr_env3) $ pip install flask-bootstrap\n",
    "```\n",
    "\n",
    "Flask 扩展一般都在创建程序实例时初始化：\n",
    "\n",
    "```python\n",
    "# hello.py\n",
    "from flask_bootstrap import Bootstrap\n",
    "\n",
    "# ...\n",
    "bootstrap = Bootstrap(app)\n",
    "```\n",
    "\n",
    "导入`Bootstrap`，然后把程序实例传入构造方法进行初始化。\n",
    "\n",
    "初始化 Flask-Bootstrap 之后，就可以在程序中使用一个包含所有 Bootstrap 文件的基模板。 这个模板利用 Jinja2 的模板继承机制，让程序扩展一个具有基本页面结构的基模板， 其中就有用来引入 Bootstrap 的元素。\n",
    "\n",
    "```django\n",
    "{# templates/base.html #}\n",
    "\n",
    "{% extends \"bootstrap/base.html\" %}\n",
    "\n",
    "{% block title %}Flaskr{% endblock %}\n",
    "\n",
    "{% block navbar %}\n",
    "    <div class=\"navbar navbar-inverse\" role=\"navigation\">\n",
    "        <div class=\"container\">\n",
    "            <div class=\"navbar-header\">\n",
    "                <button type=\"button\" class=\"navbar-toggle\"\n",
    "                        data-toggle=\"collapse\" data-target=\".navbar-collapse\">\n",
    "                        <span class=\"sr-only\">Toggle navigation</span>\n",
    "                        <span class=\"icon-bar\"></span>\n",
    "                        <span class=\"icon-bar\"></span>\n",
    "                        <span class=\"icon-bar\"></span>\n",
    "                </button>\n",
    "                <a class=\"navbar-brand\" href=\"/\">Flaskr</a>\n",
    "            </div>\n",
    "            <div class=\"navbar-collapse collapse\">\n",
    "                <ul class=\"nav navbar-nav\">\n",
    "                    <li><a href=\"/\">Home</a></li>\n",
    "                </ul>\n",
    "            </div>\n",
    "        </div>\n",
    "    </div>\n",
    "{% endblock %}\n",
    "\n",
    "{% block content %}\n",
    "    <div class=\"container\">\n",
    "        {% block page_content %}{% endblock %}\n",
    "    </div>\n",
    "{% endblock %}\n",
    "```\n",
    "\n",
    "```django\n",
    "{# templates/index.html #}\n",
    "\n",
    "{% extends \"base.html\" %}\n",
    "\n",
    "{% block page_content %}\n",
    "    <div class=\"page-header\">\n",
    "        <h1>Hello, world!</h1>\n",
    "    </div>\n",
    "{% endblock %}\n",
    "```\n",
    "\n",
    "```django\n",
    "{# templates/user.html #}\n",
    "\n",
    "{% extends \"index.html\" %}\n",
    "\n",
    "{% block page_content %}\n",
    "\t<div class=\"page-header\">\n",
    "        <h1>Hello, {{ name }}!</h1>\n",
    "    </div>\n",
    "{% endblock %}\n",
    "```\n",
    "\n",
    "Jinja2 中 的`extends`指令从 Flask-Bootstrap 中导入`bootstrap/base.html`， 从而实现模板继承。Flask-Bootstrap 中的基模板提供了一个网页框架， 引入了 Bootstrap 中的所有 CSS 和 JavaScript 文件。\n",
    "\n",
    "**🔖 执行`git checkout 2b`签出程序的这个版本。**"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "ein.tags": "worksheet-0",
    "slideshow": {
     "slide_type": "-"
    }
   },
   "source": [
    "### Flask-Bootstrap基模板中定义的Block\n",
    "\n",
    "Flask-Bootstrap 的`base.html`模板还定义了很多其他块，都可在衍生模板中使用。\n",
    "\n",
    "| block名           | 外层block | 说明               |\n",
    "|-------------------|-----------|--------------------|\n",
    "| **doc**           |          | 整个HTML文档        |\n",
    "| **html**          | **doc**  | `<html>`标签中的内容  |\n",
    "| **html\\_attribs** | **doc**  | `<html>`标签的属性    |\n",
    "| **head**          | **doc**  | `<head>`标签中的内容  |\n",
    "| **title**         | **head** | `<title>`标签中的内容 |\n",
    "| **metas**         | **head** | 一组`<meta>`标签      |\n",
    "| **styles**        | **head** | 层叠样式表定义      |\n",
    "| **body**          | **doc**  | `<body>`标签中的内容  |\n",
    "| **body\\_attribs** | **body** | `<body>`标签的属性    |\n",
    "| **navbar**        | **body** | 用户定义的导航条    |\n",
    "| **content**       | **body** | 用户定义的页面内容  |\n",
    "| **scripts**       | **body** | 文档底部的 JavaScript 声明 |\n",
    "\n",
    "下面是一些例子：\n",
    "\n",
    "-   添加自定义CSS文件\n",
    "\n",
    "    ```django\n",
    "    {% block styles %}\n",
    "        {{super()}}\n",
    "        <link rel=\"stylesheet\"\n",
    "              href=\"{{ url_for('.static', filename='mystyle.css') }}\">\n",
    "    {% endblock %}\n",
    "    ```\n",
    "\n",
    "-   在Bootstrap JavaScript文件之前自定义加载的JavaScript文件\n",
    "\n",
    "    ```django\n",
    "    {% block scripts %}\n",
    "        <script src=\"{{ url_for('.static', filename='myscripts.js') }}\"></script>\n",
    "        {{ super() }}\n",
    "    {% endblock %}\n",
    "    ```\n",
    "\n",
    "-   给`<html>`标签添加`lang`属性\n",
    "\n",
    "    ```django\n",
    "    {% block html_attribs %} lang=\"zh-CN\"{% endblock %}\n",
    "    ```\n",
    "\n",
    "\n",
    "## 自定义错误页面\n",
    "\n",
    "Flask允许程序使用基于模板的自定义错误页面。 最常见的错误代码有两个:\n",
    "\n",
    "-   **404**: 客户端请求未知页面或路由时显示\n",
    "-   **500**: 有未处理的异常时显示\n",
    "\n",
    "`hello.py`自定义错误页面：\n",
    "\n",
    "```python\n",
    "@app.errorhandler(404)\n",
    "def page_not_found(e):\n",
    "    return render_template('404.html'), 404\n",
    "# equivalent to\n",
    "# app.register_error_handler(404, page_not_found)\n",
    "\n",
    "@app.errorhandler(500)\n",
    "def internal_server_error(e):\n",
    "    return render_template('500.html'), 500\n",
    "```\n",
    "\n",
    "编写错误处理程序中引用的模板：\n",
    "\n",
    "```django\n",
    "{# templates/404.html #}\n",
    "\n",
    "{% extends 'base.html' %}\n",
    "\n",
    "{% block title %}\n",
    "    Flaskr - Page Not Found\n",
    "{% endblock %}\n",
    "\n",
    "{% block page_content %}\n",
    "\t<div class=\"page-header\">\n",
    "        <h1>Not Found</h1>\n",
    "    </div>\n",
    "{% endblock %}\n",
    "```\n",
    "\n",
    "```django\n",
    "{# templates/500.html #}\n",
    "\n",
    "{% extends 'base.html' %}\n",
    "\n",
    "{% block title %}\n",
    "\tFlaskr - Internal Server Error\n",
    "{% endblock %}\n",
    "\n",
    "{% block page_conent %}\n",
    "\t<div class=\"page-header\">\n",
    "        <h1>Internal Server Error</h1>\n",
    "    </div>\n",
    "{% endblock %}\n",
    "```\n",
    "\n",
    "**🔖 执行`git checkout 2c`签出程序的这个版本。**\n",
    "\n",
    "\n",
    "## 链接\n",
    "\n",
    "**任何具有多个路由的程序都需要可以连接不同页面的链接。**\n",
    "\n",
    "在 Python shell 中检查为`hello.py`生成的映射：\n",
    "\n",
    "```python\n",
    "(flaskr_env3) $ python\n",
    ">>> from hello import app\n",
    ">>> app.url_map\n",
    "Map([<Rule '/' (OPTIONS, GET, HEAD) -> index>,\n",
    " <Rule '/static/<filename>' (OPTIONS, GET, HEAD) -> static>,\n",
    " <Rule '/user/<name>' (OPTIONS, GET, HEAD) -> user>])\n",
    "```\n",
    "\n",
    "在模板中直接编写URL会对代码中定义的路由产生不必要的依赖关系。 如果重新定义路由，模板中的链接可能会失效。\n",
    "\n",
    "Flask 提供了`url_for()`辅助函数，它可以 **使用程序URL映射中保存的信息生成URL** 。\n",
    "\n",
    "`url_for()`函数最简单的用法是以视图函数名（或者`app.add_url_route()`定义路由时使用的端点名）作为参数，返回对应的URL。\n",
    "\n",
    "例如，在当前版本的`hello.py`程序中调用`url_for('index')`得到的结果是`/`。 调用`url_for('index', _external=True)`返回的则是绝对地址，在这个示例中是`http://localhost:5000/`。<sup><a id=\"fnr.2\" class=\"footref\" href=\"#fn.2\">2</a></sup>\n",
    "\n",
    "使用`url_for()`生成动态地址时，将动态部分作为关键字参数传入。 例如， `url_for('user', name='john', _external=True)` 的返回结果是`http://localhost:5000/user/john`。\n",
    "\n",
    "传入`url_for()`的关键字参数不仅限于动态路由中的参数。 函数能将任何额外参数添加到查询字符串中。 例如， `url_for('index', page=2)` 的返回结果是`/?page=2`。\n",
    "\n",
    "\n",
    "## 静态文件\n",
    "\n",
    "默认设置下，Flask在程序根目录中名为`static`的子目录中寻找静态文件。 如果需要，可在`static`文件夹中使用子文件夹存放文件。\n",
    "\n",
    "URL 映射中有一个`static`路由。对静态文件的引用被当成一个特殊的路由， 即`/static/<filename>`。 调用 `url_for('static', filename='css/styles.css', _external=True)` 得到的结果是`http://localhost:5000/static/css/styles.css`。\n",
    "\n",
    "下面的例子展示了如何在程序的基模板中放置`favicon.ico`图标。 这个图标会显示在浏览器的地址栏中。\n",
    "\n",
    "```django\n",
    "{% block head %}\n",
    "\t{{ super() }}\n",
    "    <link rel=\"shortcut icon\" href=\"{{ url_for('static', filename='favicon.ico') }}\"\n",
    "        type=\"image/x-icon\">\n",
    "    <link rel=\"icon\" href=\"{{ url_for('static', filename='favicon.ico') }}\"\n",
    "        type=\"image/x-icon\">\n",
    "{% endblock %}\n",
    "```\n",
    "\n",
    "图标的声明会插入`head`块的末尾。注意如何使用`super()`保留基模板中定义的块的原始内容。\n",
    "\n",
    "**🔖 执行`git checkout 2d`签出程序的这个版本。**\n",
    "\n",
    "## 脚注\n",
    "\n",
    "<sup><a id=\"fn.1\" class=\"footnum\" href=\"#fnr.1\">1</a></sup> 完整的过滤器列表可在 Jinja2 文档（ <http://jinja.pocoo.org/docs/templates/#builtin-filters> ）中查看。\n",
    "\n",
    "<sup><a id=\"fn.2\" class=\"footnum\" href=\"#fnr.2\">2</a></sup> 生成连接程序内不同路由的链接时，使用相对地址就足够了。如果要生成在浏览器之外使用的链接，则必须使用绝对地址，例如在电子邮件中发送的链接。\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "name": "python3"
  },
  "name": "3-jinja2-template.ipynb"
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
